const _ = require('lodash')
const log = require('../utils/logger')

class Executor {
    #id = 1
    #cache = new Map()
    disposable = []
    #isNotInit = true
    ttl = -1
    #ttlTimer
    logger
    logPath
    logFile
    name
    constructor(opts) {
        this.name = opts.name
        this.ttl = opts.ttl || this.ttl
        this.logPath = opts.logPath
        this.logFile = opts.logFile
        if (this.logPath && this.logFile) {
            const name = this.logFile + '.txt'
            this.logger = log.create(this.logPath, name)
            this.disposable.push(() => {
                log.close(name)
            })
        }
    }
    #refreshTimeToLive() {
        clearTimeout(this.#ttlTimer)
        if (this.ttl > 0) {
            this.#ttlTimer = setTimeout(() => {
                if (this.#cache.size > 0) {
                    this.logger?.info('尝试关闭执行器失败, 等待队列任务完成...')
                    this.#refreshTimeToLive()
                } else {
                    this.close()
                }
            }, this.ttl)
        }
    }
    #createProxyFn(target, prop, metedata) {
        let originalFn = target[prop]
        let max = originalFn.length
        return async (...args) => {
            let stime = Date.now()
            let isAsync
            try {
                if (args.length <= max) {
                    return await originalFn.call(target, ...args)
                }
                isAsync = args.at(max)
                if (isAsync) {
                    const available = args.slice(0, max)
                    return await this.send(metedata, target, prop, ...available)
                } else {
                    return await originalFn.call(target, ...args)
                }
            } finally {
                const duration = Date.now() - stime + ' ms'
                this.logger?.info({ isAsync, prop, args, duration })
            }
        }
    }
    #invokeMethodProxy(that, fn, metedata, args) {
        let max = fn.length
        let stime = Date.now()
        let isAsync
        try {
            if (args.length <= max) {
                return fn.apply(that, ...args)
            }
            isAsync = args.at(max)
            if (isAsync) {
                const available = args.slice(0, max)
                return this.send(metedata, that, '', ...available)
            } else {
                return fn.apply(that, ...args)
            }
        } finally {
            const duration = Date.now() - stime + ' ms'
            this.logger?.info({ isAsync, args, duration })
        }
    }
    createMessage(metedata, target, command, ...args) {
        const commandId = this.#id++
        const useds = this.#encodeArgs(target, args)
        const data = {
            id: commandId,
            command,
            data: useds,
            metedata
        }
        return data
    }
    clearMessage(id) {
        const item = this.#cache.get(id)
        const args = item.data
        args.forEach(it => {
            this.#cache.delete(it.id)
        })
        this.#cache.delete(id)
    }
    #encodeArgs(target, args) {
        return args.map(it => {
            if (_.isFunction(it)) {
                const id = this.#id++
                this.#cache.set(id, {
                    fn: it,
                    target
                })
                return {
                    id,
                    type: 'FUNCTION'
                }
            }
            return it
        })
    }
    #decodeReturn(response) {
        if (!response) {
            return
        }
        if (response.type == 'FUNCTION') {
            return async (...args) => {
                return await this.send({}, response, 'RETURN_FUNCTION', value.id, ...args)
            }
        }
        if (_.isPlainObject(response)) {
            return _.mapValues(response, value => {
                if (value.type == 'FUNCTION') {
                    return async (...args) => {
                        return await this.send({}, response, 'RETURN_FUNCTION', value.id, ...args)
                    }
                }
                return value
            })
        }
        return response
    }
    async send(metedata, target, prop, ...args) {
        if (this.#isNotInit) {
            this.#isNotInit = false
            await this.onInit()
        }
        const msg = this.createMessage(metedata, target, prop, ...args)
        return new Promise((resolve, reject) => {
            const command = Object.assign({}, msg)
            msg.resolve = resolve
            msg.reject = reject
            this.#cache.set(msg.id, msg)
            this.#refreshTimeToLive()
            return this.onSend(command)
        })
    }
    on(message) {
        const { id, command, data } = message
        const item = this.#cache.get(id)
        switch (command) {
            case 'ARGS_FUNCTION':
                {
                    item.fn?.(...data)
                }
                break
            case 'ERROR':
                {
                    const processError = new Error(data.message)
                    processError.stack = data.stack
                    item.reject(processError)
                    this.clearMessage(id)
                }
                break
            case 'FINISH':
                {
                    const response = this.#decodeReturn(data)
                    item.resolve(response)
                    this.clearMessage(id)
                }
                break
        }
    }
    async close() {
        if (!this.#isNotInit) {
            await this.send({}, {}, 'CLOSE')
        }
        clearTimeout(this.#ttlTimer)
        await this.logger?.info('执行器已经关闭, 资源已经释放...')
        for (const dispose of this.disposable) {
            await dispose()
        }
        this.#isNotInit = true
    }
    create(target, predicat, metedata) {
        const handler = {
            get: (target, prop, receiver) => {
                const fn = target[prop]
                if (prop == '_executor') {
                    return this
                }
                if (prop == '_dispose') {
                    return () => {
                        return this.close()
                    }
                }
                if (predicat(prop, fn)) {
                    return this.#createProxyFn(target, prop, metedata)
                } else {
                    return Reflect.get(target, prop, receiver)
                }
            },
            apply: (fn, that, args) => {
                if (predicat('', fn)) {
                    return this.#invokeMethodProxy(that, fn, metedata, args)
                } else {
                    return fn.apply(that, args)
                }
            }
        }
        return new Proxy(target, handler)
    }
    async onInit() {}
    async onSend(data) {}
}
module.exports = Executor
